Server
Client
Server
Client
Phase 1 — OPRF Blinding
Phase 2 — OPAQUE KE1 / KE2
Build login request
loop
[for each candidate in bucket]
Phase 3 — OPAQUE KE3
par
[finishLogin across all
PADDING_COUNT slots]
Derive session key
r ← random scalar
P = H(email)
blinded = r · P
blinded
oprf_key (env secret)
evaluated = oprf_key · blinded
evaluated
oprf_output = (1/r) · evaluated
h = SHA-256(oprf_output)
bucket = truncate(h, k)
(clientLoginState, ke1) = startLogin(password)
bucket, ke1
ke2[i] = startLogin(candidate[i].record)
pad to PADDING_COUNT, shuffle
responses[0..PADDING_COUNT]
finishLogin(clientLoginState, responses[0]) → null
finishLogin(clientLoginState, responses[i]) → ke3
finishLogin(clientLoginState, responses[N]) → null
ke3, candidate: i
Authenticated